#ifndef CL_PROFILER_HPP
#define CL_PROFILER_HPP

#if !defined(__CL_ENABLE_EXCEPTIONS)
#define __CL_ENABLE_EXCEPTIONS
#endif

#include "cl.hpp"
#include <cstddef>
#include <vector>
#include <iostream>

namespace {
    const double FLOPS = 16384.0; // See benchmark_fp[32/64].cl
}

namespace cl {

/** Benchmark Profiler */
template<typename T>
class Profiler {

    CommandQueue *commandQueue;
    Kernel       *kernel;
    Event        *kernelEvent;
    std::size_t  items;
    T       *hostParam0;
    T       *hostParam1;
    Buffer *deviceParam0;
    Buffer *deviceParam1;

    Profiler(const Profiler &);
    Profiler &operator=(const Profiler &);

public:

    /** @brief Profiler tests a kernel with given problem size.
     * @param context of a selected platform device
     * @param device selection
     * @param program executable
     * @param name of the kernel under test
     * @param items count of the problem size */
    Profiler(Context &context, Device &device,
             Program &program, const char *name, std::size_t items) :
        commandQueue(0),
        kernel(0),
        kernelEvent(0),
        items(0),
        hostParam0(0),
        hostParam1(0),
        deviceParam0(0),
        deviceParam1(0) {
        try {
            commandQueue = new CommandQueue(context, device, CL_QUEUE_PROFILING_ENABLE);
            kernel = new Kernel(program, name);
            this->items = items;
            hostParam0 = new T[items];
            hostParam1 = new T[items];
            for(std::size_t i=0; i<items; i++) {
                hostParam0[i] = T();
                hostParam1[i] = T();
            }
            deviceParam0 = new Buffer(context, CL_MEM_READ_ONLY, sizeof(T)*items);
            deviceParam1 = new Buffer(context, CL_MEM_WRITE_ONLY, sizeof(T)*items);
            kernel->setArg(0, *deviceParam0);
            kernel->setArg(1, *deviceParam1);
        } catch(Error &e) {
            std::cerr << "cl::Profiler::Profiler " <<
                         e.what() << "(" << e.err() << ")" << std::endl;
        }
    }

    ~Profiler(void) {
            delete kernel;
            delete deviceParam0;
            delete deviceParam1;
            if(kernelEvent) {
                delete kernelEvent;
            }
            delete commandQueue;
            delete[] hostParam0;
            delete[] hostParam1;
    }

    /** @brief run benchmark
     * @param workitemsPerWorkgroup decomposition of the item space */
    void run(std::size_t workitemsPerWorkgroup) {
        if(commandQueue &&
           hostParam0 && hostParam1 && deviceParam0 && deviceParam1) {
            try {
                NDRange globalNDRange(padWorkitems(workitemsPerWorkgroup));
                NDRange localNDRange(workitemsPerWorkgroup);
                if(kernelEvent) {
                    delete kernelEvent;
                }
                kernelEvent = new Event();
                commandQueue->enqueueWriteBuffer(    // Copy host buffer to
                            *deviceParam0, CL_FALSE, // device buffer
                            0, sizeof(T)*items,      // asynchronously from host
                            hostParam0);
                commandQueue->enqueueNDRangeKernel(  // Execute kernels
                            *kernel, NullRange,      // asynchronously from host
                            globalNDRange, localNDRange,
                            0, kernelEvent);
                commandQueue->enqueueReadBuffer(     // Copy device buffer back
                            *deviceParam1, CL_TRUE,  // to host buffer
                            0, sizeof(T)*items,      // synchronously from host
                            hostParam1);
            } catch(Error &e) {
                if(kernelEvent) {
                    delete kernelEvent;
                    kernelEvent = 0;
                }
                std::cerr << "cl::Profiler::run " <<
                             e.what() << "(" << e.err() << ")" << std::endl;
            }
        }
    }

    /** @brief elapsedTime of the benchmark
     * @return elapsed time of the execution in nanoseconds */
    double elapsedTime(void) {
        try {
            if(kernelEvent &&
              (kernelEvent->getInfo<CL_EVENT_COMMAND_EXECUTION_STATUS>() == CL_COMPLETE)) {
                cl_ulong end = kernelEvent->getProfilingInfo<CL_PROFILING_COMMAND_END>();
                cl_ulong start = kernelEvent->getProfilingInfo<CL_PROFILING_COMMAND_START>();
                return (end - start) * 1.0e-9; // Measured in nano seconds.
            }
        } catch(Error &e) {
            std::cerr << "cl::Profiler::elapsedTime " <<
                         e.what() << "(" << e.err() << ")" << std::endl;
        }
        return 0.0;
    }

    /** @brief performance measure of the benchmark
     * @return peformance rating in Flop/s */
    double performance(void) {
        double executionTime = elapsedTime();
        if(executionTime > 0.0) {
            return (FLOPS * items) / executionTime;
        }
        return 0.0;
    }

private:

    std::size_t padWorkitems(std::size_t workitemsPerWorkgroup) {
        std::size_t workitems = 0;
        if(workitems < workitemsPerWorkgroup)  // Each work group must contain at
            workitems = workitemsPerWorkgroup; // least one work item.
        while(workitems < items)               // The global number of workitems
            workitems += workitemsPerWorkgroup;// must be a multiple of the local
        return workitems;                      // size of a workgroup.
    }
};

}

#endif // CL_PROFILER_HPP
